package com.fa4j.system.login.interceptor;

import com.alibaba.fastjson2.JSON;
import com.fa4j.common.base.exception.BusinessException;
import com.fa4j.common.base.exception.CommonRequestError;
import com.fa4j.common.base.exception.CommonServerError;
import com.fa4j.common.base.exception.RequestException;
import com.fa4j.common.base.util.ModuleUtil;
import com.fa4j.common.web.annotation.ApiMethod;
import com.fa4j.common.web.model.*;
import com.fa4j.common.web.util.ContextUtil;
import com.fa4j.system.account.api.AccountLogClient;
import com.fa4j.system.account.dto.AccountLogCreateRequest;
import com.fa4j.system.app.AppClient;
import com.fa4j.system.login.LoginSessionClient;
import com.fa4j.system.login.dto.LoginSessionDTO;
import com.fa4j.system.login.model.LoginConstants;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair;
import org.springframework.util.ObjectUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;

import static org.springframework.web.servlet.HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE;

/**
 * 权限拦截处理器
 */
@Slf4j
@RequiredArgsConstructor
public class SystemLoginHandlerInterceptor implements HandlerInterceptor {

    private final AppClient appClient;
    private final LoginSessionClient loginSessionClient;
    private final AccountLogClient accountLogClient;

    private final LoadingCache<Pair<AppType/*appType*/, String/*roleCode*/>, List<String>/*apis*/> roleApiCache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(Duration.ofMinutes(30))
            .refreshAfterWrite(Duration.ofMinutes(5))
            .build(pair -> queryResourceIdsByRoleCode(pair.getLeft(), pair.getRight()));

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (handler instanceof HandlerMethod hm) {
            ApiMethod apiMethod = hm.getMethodAnnotation(ApiMethod.class);
            if (apiMethod == null) {
                return true;
            }
            ClientInfo clientInfo = ContextUtil.get(ClientInfo.class);
            AppType appType = clientInfo.getClientAppType();
            if (appType == null) {
                throw CommonRequestError.PARAM_MISS.build("appType");
            }
            String apiKey = request.getMethod() + ":" + request.getAttribute(BEST_MATCHING_PATTERN_ATTRIBUTE).toString();
            if (Arrays.stream(apiMethod.type()).noneMatch(type -> type == appType)) {
                log.warn("接口{}不支持应用类型{},不能被访问,请在注解@ApiMethod中配置属性type", apiKey, appType);
                throw CommonServerError.NOT_SUPPORT.build("接口" + apiKey);
            }
            LoginSessionDTO session = getSession(request);
            boolean isAuth = apiMethod.auth() == ApiMethod.Switch.AUTO ? appType.isAuth() : apiMethod.auth() == ApiMethod.Switch.ON;
            boolean isLogin = apiMethod.login() == ApiMethod.Switch.AUTO ? isAuth : apiMethod.login() == ApiMethod.Switch.ON;
            boolean isRecord = apiMethod.record() == ApiMethod.Switch.AUTO ? (isAuth && !"GET".equals(request.getMethod())) : apiMethod.record() == ApiMethod.Switch.ON;
            ContextUtil.set(LoginConstants.API_METHOD_RECORD, isRecord);
            if (session != null) {
                ContextUtil.set(session);
                ContextUtil.set(CommonWebConst.OPERATOR_ID, session.getAccountId());
                if (isAuth && !hasAuth(appType, apiKey, session.getAccount().getRoleCodes())) {
                    throw CommonRequestError.FORBIDDEN.build();
                }
            } else if (isLogin) {
                throw CommonRequestError.UNAUTHORIZED.build();
            }
        }
        return true;
    }

    private boolean hasAuth(AppType appType, String apiKey, List<String> roleCodes) {
        if (roleCodes != null) {
            if (roleCodes.contains(LoginConstants.ROLE_ADMIN)) {
                return true;
            }
            for (String roleCode : roleCodes) {
                List<String> roleApis = roleApiCache.get(Pair.of(appType, roleCode));
                if (roleApis != null && roleApis.contains(apiKey)) {
                    return true;
                }
            }
        }
        return false;
    }

    private LoginSessionDTO getSession(HttpServletRequest request) {
        String token = getHeaderToken(request);
        if (ObjectUtils.isEmpty(token)) {
            return null;
        }
        return loginSessionClient.getSession(token);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        Boolean isRecord = ContextUtil.get(LoginConstants.API_METHOD_RECORD);
        if (isRecord == null) return;
        LoginSessionDTO session = ContextUtil.get(LoginSessionDTO.class);
        LogInfo logInfo = ContextUtil.get(LogInfo.class);
        ClientInfo clientInfo = ContextUtil.get(ClientInfo.class);
        if (isRecord && session != null && logInfo != null && clientInfo != null) {
            AccountLogCreateRequest logDTO = toAccountLog(request, session, logInfo, clientInfo);
            // 记录操作日志
            CompletableFuture.runAsync(() -> accountLogClient.saveLog(logDTO))
                    .exceptionally(e -> {
                        log.error("保存操作日志异常:{}", logDTO, e);
                        return null;
                    });

        }
    }

    private AccountLogCreateRequest toAccountLog(HttpServletRequest request, LoginSessionDTO session, LogInfo logInfo, ClientInfo clientInfo) {
        ApiMethod apiMethod = logInfo.getMethod().getAnnotation(ApiMethod.class);
        AccountLogCreateRequest info = new AccountLogCreateRequest();
        info.setOperatorId(session.getAccountId());
        info.setAccountId(session.getAccountId());
        info.setAppType(session.getAppType());
        info.setModuleName(ModuleUtil.getModule(logInfo.getMethod().getDeclaringClass()).getName());
        info.setResourceName(apiMethod.name());
        info.setResourceMethod(request.getMethod());
        info.setResourcePath(request.getRequestURI());
        info.setClientUserAgent(clientInfo.getClientUserAgent());
        info.setClientIp(clientInfo.getClientIp());
        info.setStartTime(Instant.ofEpochMilli(logInfo.getStartTime()).atZone(ZoneId.systemDefault()).toLocalDateTime());
        info.setEndTime(Instant.ofEpochMilli(logInfo.getEndTime()).atZone(ZoneId.systemDefault()).toLocalDateTime());
        info.setParams(JSON.toJSONString(logInfo.getParams()));
        if (logInfo.getThrowable() == null) {
            info.setSuccess(true);
            info.setResult(JSON.toJSONString(logInfo.getResult()));
        } else {
            info.setSuccess(false);
            if (logInfo.getThrowable() instanceof RequestException exception) {
                info.setResult(JSON.toJSONString(ResponseVO.error(exception.getErrorType().getErrCode(), exception.getMessage())));
            } else if (logInfo.getThrowable() instanceof BusinessException exception) {
                ResponseVO<Object> responseVO = ResponseVO.error(exception.getErrorType().getErrCode(), exception.getMessage());
                responseVO.setData(exception.getErrData());
                info.setResult(JSON.toJSONString(responseVO));
            } else {
                List<String> stackTrace = Arrays.stream(logInfo.getThrowable().getStackTrace())
                        .filter(s -> !s.getClassName().startsWith("org.springframework"))
                        .map(StackTraceElement::toString)
                        .toList();
                info.setResult(JSON.toJSONString(stackTrace));
            }
        }
        return info;
    }


    private String getHeaderToken(HttpServletRequest request) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            return authHeader.substring(7);
        }
        return null;
    }

    private List<String> queryResourceIdsByRoleCode(AppType appType, String roleCode) {
        return appClient.queryApisByRoleCode(appType.name(), roleCode);
    }
}
